package com.jxpanda.commons.base;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.jxpanda.commons.constant.StringConstant;
import com.jxpanda.commons.toolkit.ObjectKit;
import com.jxpanda.commons.toolkit.ReflectionKit;
import com.jxpanda.commons.toolkit.RegexKit;
import com.jxpanda.commons.toolkit.StringKit;
import com.jxpanda.commons.toolkit.json.DefaultValueModule;
import com.jxpanda.commons.toolkit.json.JsonKit;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 【实体类】【Json类型字段】的转换器
 * 这个类是用在数据访问层，mybatis序列化\反序列化json类型的时候用的
 * 与JsonKit中的ObjectMapper是不一样的逻辑
 * 主要包含以下几个处理：
 * 1：过滤null的字段，所有值为null的字段会在序列化后消失 {"name":null,"age":30} -> {"age":30}
 * 2：过滤Entity类上的所有基础字段，包括createdDate、updatedDate、deleted、effective、creatorId、updaterId、version
 *
 * @author Panda
 */
@AllArgsConstructor
public class EntityJsonTypeHandler<T> extends BaseTypeHandler<T> {

    private final Class<T> type;

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, toJson(parameter));
    }

    @Override
    public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        String tableName = resultSet.getMetaData().getTableName(1);
        return fromJson(resultSet.getString(columnName), columnKey(tableName, columnName));
    }

    @Override
    public T getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        String tableName = resultSet.getMetaData().getTableName(1);
        String columnName = resultSet.getMetaData().getColumnName(columnIndex);
        return fromJson(resultSet.getString(columnIndex), columnKey(tableName, columnName));
    }

    @Override
    public T getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        String tableName = callableStatement.getMetaData().getTableName(1);
        String columnName = callableStatement.getMetaData().getColumnName(columnIndex);
        return fromJson(callableStatement.getString(columnIndex), columnKey(tableName, columnName));
    }

    private String columnKey(String tableName, String columnName) {
        return tableName + StringConstant.DOT + columnName;
    }

    @SneakyThrows
    protected T fromJson(String json, String columnKey) {
        boolean isArray = false;
        Type type = JsonFieldScanner.TYPE_MAP.get(columnKey);
        Type fieldType = type == null ? this.type : type;
        // 只判断List类型即可，约定了只用List来包装json数组
        if (Objects.equals(type, List.class)) {
            isArray = true;
        }
        json = StringKit.isBlank(json) ? (isArray ? StringConstant.EMPTY_ARRAY : StringConstant.EMPTY_OBJECT) : json;
        return jackson().readValue(json, new TypeReference<>() {
            @Override
            public Type getType() {
                return fieldType;
            }
        });
    }

    @SneakyThrows
    protected String toJson(T object) {
        return jackson().writeValueAsString(object);
    }

    /**
     * 懒加载单例模式
     */
    protected static ObjectMapper jackson() {
        return JacksonSingleton.INSTANCE;
    }

    /**
     * 创建ObjectMapper对象的单例模式
     */
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    private static final class JacksonSingleton {
        private static final ObjectMapper INSTANCE;

        static {
            INSTANCE = new ObjectMapper();
            INSTANCE.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            INSTANCE.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
            INSTANCE.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
            INSTANCE.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

            INSTANCE.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);

            INSTANCE.registerModule(JsonKit.javaTimeModule());

            INSTANCE.registerModule(new DefaultValueModule());

            // 这个过滤器过滤Entity基类的通用属性，
            // 设计上，为了节省存储空间，进行冗余的过程中，基类属性是不冗余保存的
            INSTANCE.setFilterProvider(new SimpleFilterProvider()
                            .addFilter(Entity.JACKSON_FILTER_ID, SimpleBeanPropertyFilter.serializeAllExcept(Entity.JACKSON_FILTER_LIST)))
                    // 给所有的Entity类都加过滤
                    .addMixIn(Entity.class, JacksonSingleton.EntityFilter.class);

        }

        @JsonFilter(Entity.JACKSON_FILTER_ID)
        private interface EntityFilter {

        }

    }

    @Getter
    @Setter
    @NoArgsConstructor
    @Accessors(chain = true)
    public static class AmountChange {
        private String bizId;
        private String bizType;
        private Integer rank;
        private String reason;
        private BigDecimal value;

        @Override
        public String toString() {
            return "AmountChange{" +
                    "bizId='" + bizId + '\'' +
                    ", bizType='" + bizType + '\'' +
                    ", rank=" + rank +
                    ", reason='" + reason + '\'' +
                    ", value=" + value +
                    '}';
        }
    }

    public static void main(String[] args) {
        String json = "[{\"bizId\":null,\"rank\": 0, \"value\": 58.0, \"reason\": \"订单合计\"}, {\"rank\": 2, \"value\": -18.0, \"reason\": \"优惠券抵扣\"}, {\"rank\": 4, \"value\": -18.0, \"reason\": \"折扣合计\"}, {\"rank\": 5, \"value\": 40.0, \"reason\": \"应付\"}]";

        try {
            List<AmountChange> list = jackson().readValue(json, new TypeReference<>() {
            });
            list.forEach(it -> {
                System.out.println(it);
            });
            System.out.println(jackson().writeValueAsString(list));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 扫描所有的Json字段
     * 扫描策略是，找到所有被 @TableName 注解的类
     * 然后找到类里面所有被 @TableField 并且 typeHandler属性是EntityJsonTypeHandler.class的字段
     * 然后把这个字段 按照 表名.字段名作为key值，字段的原始类型作为value 缓存到一个Map中
     * 这样就可以实现在运行中动态查找json字段类型的功能
     * 解决json字段反序列化过程中的Java泛型擦除问题
     */
    private static final class JsonFieldScanner {
        /**
         * 缓存的Map
         * key  是 ${table.filedName} 的格式 例如：sku.specs
         * value 是字段对应的【原始类型】例如：java.util.List<xx.xx.xx.xx.Spec>
         * 正因为扫描到了【原始类型】在反序列化的时候才能正确的处理泛型
         */
        private static final Map<String, Type> TYPE_MAP;
        /**
         * 文件协议，本地调试的时候会有
         */
        private static final String PROTOCOL_FILE = "file";
        /**
         * jar包协议，生产环境运行基本就都是从jar包里面找了
         */
        private static final String PROTOCOL_JAR = "jar";
        /**
         * 为了提升扫描速度，约定的数据访问层jar包名
         * 约定了所有的entity都由das层来处理
         * das层打成jar包，api层引入使用
         * 所有jar包名包含这个名称的包都扫面一遍，可以大大减少扫描的范围
         */
        private static final String CONVENTION_DAS_PACKAGE = "data-access-service";

        /**
         * class文件的后缀
         */
        private static final String CLASS_SUFFIX = ".class";

        /**
         * 实体类正则表达式的字符串形式
         * 不能直接用Pattern编译的原因是
         * File.separator 在 linux 系统下是 '/' 在 win系统下是 '\'
         * 然而 '\' 在正则表达式里面是转义字符...需要再额外加一个 '\' 写成 '\\' 才正确
         */
        private static final String ENTITY_CLASS_REGEX_STRING = File.separator + "model" + File.separator + "entity" + File.separator;

        /**
         * 实体类的正则
         * 约定，所有的实体都放在model/entity包下
         */
        private static final Pattern ENTITY_CLASS_REGEX = Pattern.compile(ENTITY_CLASS_REGEX_STRING.replace("\\", "\\\\") + "[\\w|$]+" + CLASS_SUFFIX);

        static {
            TYPE_MAP = new HashMap<>();
            scanClasses().forEach(clazz -> TYPE_MAP.putAll(scanFiled(clazz)));
        }

        private static Map<String, Type> scanFiled(Class<?> clazz) {
            String tableName = clazz.getAnnotation(TableName.class)
                    .value()
                    .replace(StringConstant.BACK_QUOTE, StringConstant.BLANK);
            List<Field> fieldList = ReflectionKit.getFieldList(clazz);
            return fieldList.stream()
                    .filter(it -> {
                        TableField annotation = it.getAnnotation(TableField.class);
                        return annotation != null
                                && ObjectKit.isNotEmpty(annotation.value())
                                && Objects.equals(annotation.typeHandler(), EntityJsonTypeHandler.class);
                    })
                    .collect(Collectors.toMap(it -> {
                        String columnName = it.getAnnotation(TableField.class).value().replace(StringConstant.BACK_QUOTE, StringConstant.BLANK);
                        return tableName + StringConstant.DOT + columnName;
                    }, Field::getGenericType));
        }

        private static Set<Class<?>> scanClasses() {
            return scanClasses(StringConstant.BLANK);
        }

        /**
         * Entity类扫描
         * 约定！所有Entity类都需要用@TableName来修饰
         * 因此，这个函数只扫面被@TableName修饰了的类，其他类一概无视
         * 因为要获取表名来做key值，所有@TableName上的value字段必须有值
         * 因为要开启Json字段的自动序列化/反序列化，所以@TableName上会开启 autoResultMap = true
         * 因此还会强加一条过滤条件
         * 总结：
         * 这个函数会且只会扫面所有被【@TableName("${tableName}", autoResultMap = true)】注解修饰的类
         *
         * @param packageNames 扫描的包名，这个参数可以不传
         *                     传递的话，就只从这些包里找，其他的不管
         *                     不传递的话，从 CONVENTION_DAS_PACKAGE 默认约定的包里找，如果没找到这个包，会进行全局扫描一次
         *                     【为了提升扫描的效率，只扫描包名中含有model/entity/*.class的包 使用正则式 ENTITY_CLASS_REGEX 进行匹配】
         */
        @SneakyThrows
        private static Set<Class<?>> scanClasses(String... packageNames) {
            Set<Class<?>> classes = new HashSet<>();
            if (packageNames == null) {
                packageNames = new String[]{StringConstant.BLANK};
            }
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            for (String packageName : packageNames) {
                if (!packageName.isBlank()) {
                    String packagePath = packageName.replace(StringConstant.DOT, File.separator);
                    URL resource = classLoader.getResource(packagePath);
                    findClasses(classes, packagePath, resource);
                } else {
                    Enumeration<URL> resources = classLoader.getResources(packageName);
                    while (resources.hasMoreElements()) {
                        URL resource = resources.nextElement();
                        if (resource.getPath().contains(CONVENTION_DAS_PACKAGE)) {
                            findClasses(classes, StringConstant.BLANK, resource);
                        }
                    }
                    // 检查一下是否扫描到了，如果列表是空的，说明没有扫描到，尝试全局扫描一次
                    if (classes.isEmpty()) {
                        resources = classLoader.getResources(packageName);
                        while (resources.hasMoreElements()) {
                            URL resource = resources.nextElement();
                            findClasses(classes, StringConstant.BLANK, resource);
                        }
                    }
                }
            }

            return classes.stream().filter(clazz -> {
                TableName annotation = clazz.getAnnotation(TableName.class);
                return annotation != null && ObjectKit.isNotEmpty(annotation.value()) && annotation.autoResultMap();
            }).collect(Collectors.toSet());
        }

        /**
         * 从资源里查找类
         */
        @SneakyThrows
        private static void findClasses(Set<Class<?>> classes, String packagePath, URL url) {
            if (url != null) {
                String protocol = url.getProtocol();
                if (PROTOCOL_FILE.equals(protocol)) {
                    Path path = Path.of(url.toURI());
                    // 使用NIO来遍历文件，可以很方便的查找文件，该函数会自动递归
                    Files.walkFileTree(path, new SimpleFileVisitor<>() {
                        @Override
                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                            String fileName = file.toString();
                            if (RegexKit.isFind(ENTITY_CLASS_REGEX, fileName)) {
                                String replacePath = path.toString().replace(packagePath, StringConstant.BLANK);
                                if (!replacePath.endsWith(File.separator)) {
                                    replacePath += File.separator;
                                }
                                Class<?> loadClass = loadClass(fileName.replace(replacePath, StringConstant.BLANK));
                                if (loadClass != null) {
                                    classes.add(loadClass);
                                }
                            }
                            return FileVisitResult.CONTINUE;
                        }
                    });
                } else if (PROTOCOL_JAR.equals(protocol)) {
                    JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
                    Set<? extends Class<?>> loadClasses = jarFile.stream()
                            .filter(jar -> RegexKit.isFind(ENTITY_CLASS_REGEX, jar.getRealName()))
                            .map(jar -> loadClass(jar.getRealName()))
                            .filter(Objects::nonNull)
                            .collect(Collectors.toSet());
                    classes.addAll(loadClasses);
                }
            }
        }

        /**
         * 把class文件加载成类
         */
        @SneakyThrows
        private static Class<?> loadClass(String fileName) {
            if (ObjectKit.isNotEmpty(fileName)) {
                String className = fileName
                        // '/' 替换成 '.'
                        .replace(File.separator, StringConstant.DOT)
                        // '.class' 替换成 ''
                        .replace(CLASS_SUFFIX, StringConstant.BLANK);
                return Class.forName(className);
            }
            return null;
        }

    }

}
